package com.example.demo.aop;

import com.alibaba.fastjson.JSONObject;
import com.example.demo.common.Func;
import com.example.demo.common.constant.RedisKey;
import com.example.demo.queue.producer.LogEventProducer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.CodeSignature;
import org.slf4j.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

@Aspect
@Component
@Order(1)
@RequiredArgsConstructor
@Slf4j
public class LogAop {
    /*@Autowired
    private AsyncService asyncService;
    @Autowired
    private final ApplicationContext applicationContext;*/
    private final LogEventProducer logEventProducer;
    private final RedisTemplate<String, String> redis;

    private Long startTime;
    private Long endTime;
    private String visitTime;
    private String paramInfo;
    //private JSONObject jsLog;

    //@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
    @Pointcut("execution(public * com.example.demo.controller.*.*(..))")
    public void LogAop() {

    }

    /**
     * 当有多个接口要排除时，如何优雅的写法？
     */
    /*@Pointcut("execution(public * com.example.demo.controller.CompanyController.getCompanyList(..))")
    public void LogAop2() {

    }

    @Pointcut("LogAop() && !LogAop2()")
    public void LogAop3() {

    }*/

    /**
     * 前置通知：
     * 1. 在执行目标方法之前执行，比如请求接口之前的登录验证;
     * 2. 在前置通知中设置请求日志信息，如开始时间，请求参数，注解内容等
     */
    @Before("LogAop()")
    public void before(JoinPoint joinPoint) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        startTime = System.currentTimeMillis();
        visitTime = Func.getNow().toString();

        /*log.info("url：{}" , request.getRequestURL().toString());
        log.info("{} {} {}", Func.getIP(request), request.getContentType(), request.getMethod());
        *//*log.info("method：{}" , request.getMethod());
        log.info("ip：{}" , Func.getIP(request));
        log.info("type：{}", request.getContentType());*//*
        log.info("starttime：{}" , Func.getNow());*/

        // 打印接口参数（若参数校验不通过，只能打印RequestParam参数，无法打印DTO参数）
        Object[] paramValues = joinPoint.getArgs();
        String[] paramNames = ((CodeSignature)joinPoint.getSignature()).getParameterNames();
        if (request.getContentType() != null) {
            if (request.getContentType().equals("application/x-www-form-urlencoded")) {
                JSONObject params = new JSONObject();
                for (int i = 0; i < paramNames.length; i++) {
                    params.put(paramNames[i], paramValues[i]);
                }
                //log.info("params：{}", params.toJSONString());
                paramInfo = params.toJSONString();
            }
            else if (request.getContentType().equals("application/json")) {
                //log.info("params：{}", JSONObject.toJSONString(paramValues).replaceAll("\\[|\\]", ""));
                paramInfo = JSONObject.toJSONString(paramValues).replaceAll("\\[|\\]", "");
            }
            /*else {
                for (int i = 0; i < paramNames.length; i++) {
                    log.info(paramNames[i] + ":" + paramValues[i]);
                }
            }*/
        }
    }

    /**
     * 返回通知：
     * 1. 在目标方法正常结束之后执行
     * 2. 在返回通知中补充请求日志信息，如返回时间，方法耗时，返回值，并且保存日志信息
     * 注：使用多线程或事件机制，将日志写入时，原日志中的跟踪traceId会丢失
     * 注：用事件机制记录日志：定义事件类、事件监听器，在需要触发事件时，利用上下文变量发布事件
     */
    @AfterReturning(pointcut = "LogAop()", returning = "result")
    public void afterReturning(Object result) {
        endTime = System.currentTimeMillis();
        //log.info("endtime：{}" , LocalDateTime.now());
        /*log.info("waittime：{}ms" , (endTime - startTime));
        log.info("content：{}" , result);*/

        printLog(result, "success");

        /*asyncService.writeLog(jsLog, "success");

        ApplicationEvent event = new SysLogEvent(jsLog);
        applicationContext.publishEvent(event);*/
    }

    /**
     * 异常通知：
     * 1. 在目标方法非正常结束，发生异常或者主动抛出异常时执行
     * 2. 在异常通知中设置异常信息，并将其保存
     *
     * 注：
     * 1、被切入方法异常时才执行，该AOP切面异常时，出错信息要到日志中看
     * 2、切面内方法异常时，会转到相应的异常处理类执行
     */
    @AfterThrowing(pointcut = "LogAop()", throwing = "t")
    public void afterThrowing(Throwable t) {
        endTime = System.currentTimeMillis();
        //log.info("errortime：{}" , LocalDateTime.now());
        /*log.info("waittime：{}ms" , (endTime - startTime));
        log.info("errorinfo：{}" , t.getMessage());*/

        printLog(t.getMessage(), "error");

        /*asyncService.writeLog(jsLog, "success");

        ApplicationEvent event = new SysLogEvent(jsLog);
        applicationContext.publishEvent(event);*/
    }

    /**
     * 后置通知：
     * 1、无论目标方法正常，还是异常，都会执行，类似finally
     * 2、如果切面内方法异常时，也会执行
     **/
    @After("LogAop()")
    public void after(JoinPoint joinPoint) {
        //Func.print("执行 @After");
    }

    /**
     * 打印本次访问的日志
     **/
    private void printLog(Object result, String returnType) {
        /*if (returnType.equals("success")) {
            Func.print("执行 @AfterReturning");
        }
        else {
            Func.print("执行 @AfterThrowing");
        }*/

        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String ip = Func.getIP(request);
        Long waitTime = endTime - startTime;

        JSONObject logInfo = new JSONObject();
        logInfo.put("logId", MDC.get("traceId"));
        logInfo.put("userid", redis.opsForValue().get(RedisKey.LOG_USERID + MDC.get("traceId")));
        logInfo.put("name", redis.opsForValue().get(RedisKey.LOG_NAME + MDC.get("traceId")));
        logInfo.put("url", request.getRequestURL().toString());
        logInfo.put("ip", ip);
        logInfo.put("contentType", request.getContentType());
        logInfo.put("method", request.getMethod());
        logInfo.put("visitTime", visitTime);
        logInfo.put("endTime", Func.getNow().toString());
        logInfo.put("waitTime", waitTime + "ms");
        logInfo.put("param", paramInfo);
        logInfo.put("result", returnType.equals("success") ? JSONObject.toJSONString(result) : result);
        logInfo.put("message", redis.opsForValue().get(RedisKey.lOG_ERROR_MSG + MDC.get("traceId")));
        logInfo.put("returnType", returnType);

        // 将访问详情记录以异步方式插入MongoDB
        logEventProducer.insertLog(logInfo);

        // 删除转存redis的中介值
        redis.delete(RedisKey.LOG_USERID + MDC.get("traceId"));
        redis.delete(RedisKey.LOG_NAME + MDC.get("traceId"));
        redis.delete(RedisKey.lOG_ERROR_MSG + MDC.get("traceId"));

        // 将访问简略记录打印到日志中
        log.info("url:{} ip:{} visitTime:{} waitTime:{}ms",
                request.getRequestURL().toString(),
                ip,
                visitTime,
                waitTime);

        if (waitTime > 1000) {
            log.warn("url:{} ip:{} visitTime:{} waitTime:{}ms",
                    request.getRequestURL().toString(),
                    ip,
                    visitTime,
                    waitTime);
        }
    }
}
